Jetpack Splashscreen 解析 | 助力新生代 IT 农民工 事半功倍
hi
这是 dhl
的第 39
篇文章
Jetpack 家族迎来了一位新的成员 Core Splashscreen,所以我也要重新开始写 Jetpack 系列文章了,在这之前写过一系列 Jetpack 文章以及配套的实战应用,包含 App Startup
、 Paging3
、 Hilt
、 DataStore
、ViewBinding
等等实战项目,点击下方链接前去查看。
AndroidX-Jetpack-Practice 系列实战项目
https://github.com/hi-dhl/AndroidX-Jetpack-PracticeKotlin 插件的落幕,ViewBinding 的崛起 竟然如此简单,DataBinding 和 ViewBinding 再见 SharedPreferences 拥抱 Jetpack DataStore (一) 再见 SharedPreferences 拥抱 Jetpack DataStore (二) Jetpack 成员 AndroidX App Startup 实践以及原理分析 Jetpack 成员 Paging3 数据库实践以及源码分析(一) Jetpack 成员 Paging3 网络实践及原理分析(二) Jetpack成员Paging3获取网络分页数据并更新到数据库中(三) Jetpack 成员 Hilt 实践(一)启程过坑记 Jetpack 成员 Hilt 实践之 App Startup(二)进阶篇 Jetpack 成员 Hilt 与 Dagger 大不同(三)落地篇 全方面分析 Hilt 和 Koin 性能 Jetpack 实战:神奇宝贝
而今天这篇文章主要介绍 Google 新库 Core Splashscreen ,众所周知在 Android 12 中增加了一个改善用户体验的功能 SplashScreen API,它可为所有应用添加启动画面。包括启动时进入应用的启动动画,以及退出动画。
通过这篇文章你将学习到以下内容
Core Splashscreen 解决了什么问题? Core Splashscreen 工作原理? 针对不同的场景,如何在项目中使用 Core Splashscreen? Core Splashscreen 源码分析?
Core Splashscreen 实战项目地址,可以前往 GitHub 查看示例项目 Splashscreen。
https://github.com/hi-dhl/AndroidX-Jetpack-Practice
Core Splashscreen
Core Splashscreen 解决了什么问题?
在 Android 启动过程中会出现白屏 / 黑屏,为了改善这一体验,因此添加启动画面,从而改善视觉上的体验,为了实现这一功能,市面上也有很多实现方法,都有各自的优缺点,因此并不能保证在所有设备上都能够流畅的运行。
其次有的时候需要从本地磁盘或者网络异步加载数据,等待数据加载完之后,才会去渲染 View, 大多数时候,希望将数据加载提前,尽量保证用户进入到首页之后,看到数据,减少用户的等待时间。
在 Android 12 上新增的 SplashScreen API,可以解决这一系列问题,但是缺点是仅限于 Android 12。
Core Splashscreen 因此而诞生了,为 Android 12 新增的 SplashScreen API 提供了向后兼容,可以在 Android 5.0 (API 21) ~ Android 12 (API 31)所有的 API 上使用。来看一下 Google 提供的动画效果。
Core Splashscreen 工作原理
Core Splashscreen 为 Android 12 新增的 SplashScreen API 提供了向后兼容,但是仅仅在以下情况下才会显示启动画面:
冷启动:用户打开 APP 时 APP 进程尚未运行 温启动:APP 进程正在运行,但是 Activity 尚未创建
启动动画只有在以上情况才会显示,但是在热启动期间是不会显示启动画面。
热启动:APP 进程正在运行,Activity 也已经创建,也就说用户按下 Home 键退到后台,直到 Activity 被销毁之前,是不会显示启动画面
如何使用 Core Splashscreen
因为 Core Splashscreen 兼容了 Android 12 新增的 SplashScreen API, 因此需要将 compileSdkVersion 更新到 31 及其以上。
如果你的 SDK 还没有更新到 Android 12, 请先更新。SDK Manager -> 选择 Android 12
android {
compileSdkVersion 31
}
在模块级别的 build.gradle
文件中添加以下依赖。
implementation 'androidx.core:core-splashscreen:1.0.0-alpha01'
当添加完依赖之后就可以开始使用 Core Splashscreen,只需要三步即可实现显示启动画面。
1. 在 res/values/themes.xml
文件下添加新的主题 Theme.AppSplashScreen
<style name="Theme.AppSplashScreen" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/purple_200</item>
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item>
<item name="postSplashScreenTheme">@style/Theme.AppTheme</item>
</style>
<!-- Base application theme. -->
<style name="Theme.AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- 添加 APP 默认主题 -->
</style>
android:windowSplashScreenBackground
: 设置背景颜色windowSplashScreenAnimatedIcon
: 设置显示在屏幕中间的图标, 如果是通过AnimationDrawable
和AnimatedVectorDrawable
创建的对象,可呈现动画效果,则会在页面显示的时候,播放动画postSplashScreenTheme
: 设置显示动画不可见时,使用 APP 的默认主题
2. 在 application
节点中,设置上一步添加主题 Theme.AppSplashScreen
<application
android:theme="@style/Theme.AppSplashScreen">
</application>
3. 在调用 setContentView()
方法之前调用 installSplashScreen()
class MainActivity : AppCompatActivity() {
private val binding: ActivityMainBinding by viewbind()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
installSplashScreen()
with(binding) {
// init view
}
}
}
调用 installSplashScreen()
方法主要将 Activity 与我们添加的主题相关联。这一步完成之后,就可以在 APP 启动过程中,看到刚才设置的图标或者动画了。
扩展功能
让启动动画持久一点
默认情况下当应用绘制第一帧后,启动画面会立即关闭,但是有的时候需要从本地磁盘或者网络异步加载数据,这个时候,希望启动画面能够等到数据加载完回来才结束。可以通过以下方法实现。
splashScreen.setKeepVisibleCondition { !appReady }
// 模拟从本地磁盘或者网络异步加载数据的耗时操作
Handler(Looper.getMainLooper())
.postDelayed({ appReady = true }, 3000)
调用以上方法,可以让应用暂停绘制第一帧这样启动画面就不会结束,当数据加载完之后,通过更新变量 appReady
来控制是否结束启动画面。
实现退出动画
当然我们也可以添加启动画面的退出动画,即从启动画面优雅的回到应用主界面。
splashScreen.setOnExitAnimationListener { splashScreenViewProvider ->
......
// 自定义退出动画
val translationY = ObjectAnimator.ofFloat(......)
translationY.doOnEnd { splashScreenViewProvider.remove() }
translationY.start()
}
效果可以前往 GitHub 查看示例项目 Splashscreen。
https://github.com/hi-dhl/AndroidX-Jetpack-Practice
Core Splashscreen 源码解析
Core Splashscreen 源码很简单,总共就只有两个类。
SplashScreen
:主要为实现 SplashScreen API 提供了向后兼容性,用于将 Activity 与主题相关联。SplashScreenViewProvider
: 用于控制退出动画(启动画面 -> 应用主界面),当退出动画结束时需要手动调用SplashScreenViewProvider#remove()
方法
初始化 SplashScreen
通过调用 SplashScreen#installSplashScreen()
方法来进行初始化,将 Activity 与添加的主题相关联。
androidx/core/splashscreen/SplashScreen.kt
public companion object {
@JvmStatic
public fun Activity.installSplashScreen(): SplashScreen {
val splashScreen = SplashScreen(this)
splashScreen.install()
return splashScreen
}
}
private fun install() {
impl.install()
}
最终都是通过调用 impl.install()
方法来进行初始化,一起来看看成员变量 impl
是如何初始化的。
private val impl = when {
SDK_INT >= 31 -> Impl31(activity)
SDK_INT == 30 && PREVIEW_SDK_INT > 0 -> Impl31(activity)
SDK_INT >= 23 -> Impl23(activity)
else -> Impl(activity)
}
到这里我们知道了 Google 为了向后兼容,针对于不同版本的系统,分别对应有不同的实现类。最终都是调用 install()
方法来进行初始化的,在 install()
方法内通过解析我们添加的主题,最后通过 activity.setTheme()
方法,将添加的主题和 Activity 关联在一起。
如何让启动动画持久一点
在代码中,我们通过调用 SplashScreen#setKeepVisibleCondition()
方法,让启动动画持久一点,等待数据加完之后,才结束启动动画。一起来看看这个方法。
androidx/core/splashscreen/SplashScreen.kt
public fun setKeepVisibleCondition(condition: KeepOnScreenCondition) {
// impl:针对于不同版本的系统,分别对应有不同的实现类
impl.setKeepVisibleCondition(condition)
}
open fun setKeepVisibleCondition(keepOnScreenCondition: KeepOnScreenCondition) {
......
observer.addOnPreDrawListener(object : OnPreDrawListener {
override fun onPreDraw(): Boolean {
if (splashScreenWaitPredicate.shouldKeepOnScreen()) {
return false
}
contentView.viewTreeObserver.removeOnPreDrawListener(this)
// 当开始绘制时,会调用 dispatchOnExitAnimation 方法,结束启动动画
mSplashScreenViewProvider?.let(::dispatchOnExitAnimation)
return true
}
})
}
最后通过 ViewTreeObserver 来监听视图的变化,当视图将要开始绘制时,会回调 OnPreDrawListener#onPreDraw()
方法。最后调用 dispatchOnExitAnimation
方法,结束启动动画。
实现退出动画
最后一起来看一下,源码中是如何实现退出动画,即从启动画面优雅的回到应用主界面,源码中只是提供了一个 OnExitAnimationListener
接口,将退出动画交给了开发者去实现,一起来看一下SplashScreen#setOnExitAnimationListener()
方法。
androidx/core/splashscreen/SplashScreen.kt
Android 12 以上
override fun setOnExitAnimationListener(
exitAnimationListener: OnExitAnimationListener
) {
activity.splashScreen.setOnExitAnimationListener {
val splashScreenViewProvider = SplashScreenViewProvider(it, activity)
exitAnimationListener.onSplashScreenExit(splashScreenViewProvider)
}
}
在 Android 12 中是通过系统源码提供的接口 activity.splashScreen.setOnExitAnimationListener
,回调对外暴露的接口 OnExitAnimationListener
让开发者去实现退出动画的效果。
Android 12 以下
open fun setOnExitAnimationListener(exitAnimationListener: OnExitAnimationListener) {
animationListener = exitAnimationListener
val splashScreenViewProvider = SplashScreenViewProvider(activity)
......
splashScreenViewProvider.view.addOnLayoutChangeListener(
object : OnLayoutChangeListener {
override fun onLayoutChange(......) {
......
dispatchOnExitAnimation(splashScreenViewProvider)
}
})
}
fun dispatchOnExitAnimation(splashScreenViewProvider: SplashScreenViewProvider) {
......
splashScreenViewProvider.view.postOnAnimation {
finalListener.onSplashScreenExit(splashScreenViewProvider)
}
}
通过向屏幕中显示的 View 添加 addOnLayoutChangeListener
方法,来监听布局的变化,当布局会发生改变时,会回调 onLayoutChange
方法,最后通过回调对外暴露的接口 OnExitAnimationListener
让开发者去实现退出动画。
不过这里需要注意的是,最后都需要调用 SplashScreenViewProvider#remove()
方法在合适的时机移除动画,可以在退出动画结束时,调用这个方法。
总结
本文从不同的角度分别分析了 Core Splashscreen。如何在项目中使用 Core Splashscreen,可以前往 GitHub 查看示例项目 Splashscreen。
仓库地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice
另外 KtKit
是用 Kotlin 语言编写的小巧而实用工具库,包含了项目中常用的一系列工具,我添加了许多新的功能,包含了很多 Kotlin 技巧。文章分析可前往查看 为数不多的人知道的 Kotlin 技巧以及解析(三)。
监听 EditText
将 Flow 通过 lifecycleScope 将 EditText 与 Activity / Fragment 的生命周期绑定在一起,在 Activity / Fragment 生命周期结束时,会结束 flow , flow 结束时会断开它们之间的引用,有效的避免内存泄漏。
......
// 监听 TextWatcher#onTextChanged 的回调函数
editText.textChange(lifecycleScope) {
Log.e(TAG, "textChange = $it")
}
// 监听 TextWatcher#beforeTextChanged 的回调函数
editText.textChangeWithbefore(lifecycleScope) {
Log.e(TAG, "textChangeWithbefore = $it")
}
// 监听 TextWatcher#afterTextChanged 的回调函数
editText.textChangeWithAfter(lifecycleScope) {
Log.e(TAG, "textChangeWithbefore = $it")
}
......
监听蜂窝网络变化
lifecycleScope.launch {
listenCellular().collect {
Log.e(TAG, "listenNetwork = $it")
}
}
监听 wifi 网络的变化
lifecycleScope.launch {
listenWifi().collect {
Log.e(TAG, "listenNetwork = $it")
}
}
监听蓝牙网络的变化
lifecycleScope.launch {
listenNetworkFlow().collect {
Log.e(TAG, "listenNetwork = $it")
}
}
更多 API 使用方式点击这里前往查看:
KtKit 仓库地址:https://github.com/hi-dhl/KtKit KtKit 在线阅读:https://ktkit.hi-dhl.com
如果这个仓库对你有帮助,请在仓库右上角帮我 star 一下,非常感谢你的支持,同时也欢迎你提交 PR ❤️❤️❤️
如果有帮助欢迎 在看 、点赞 、分享 就是对我最大的鼓励
代码不止,文章不停
欢迎点击下方卡片关注我,持续分享最新的技术
推荐阅读:
最后推荐我一直在更新维护的项目和网站:
个人博客,将所有文章进行分类,欢迎前去查看
https://hi-dhl.com最新的 AndroidX Jetpack 相关组件的实战项目 以及 原理分析的文章
https://github.com/hi-dhl/AndroidX-Jetpack-PracticeLeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析
剑指 offer:https://offer.hi-dhl.com
LeetCode:https://leetcode.hi-dhl.com最新 Android 10 源码分析系列文章
https://github.com/hi-dhl/Android10-Source-Analysis一系列国外的技术文章,每篇文章都会有译者思考部分,对原文的更加深入的分析
https://github.com/hi-dhl/Technical-Article-Translation「为互联网人而设计,国内国外名站导航」涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等网址
https://site.51git.cn